iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0
Modern Web

Rails,我要進來囉系列 第 6

第六天:躲在 Rails 背後默默付出的幕後功臣 - ActiveJob

  • 分享至 

  • xImage
  •  

開場白

鼬~~哩賀,我是寫程式的山姆老弟,昨天跟大家一起看了一點 ActiveSupport 的 source code,今天就來看一下 ActiveJob 是在幹嘛的吧,夠夠~

https://github.com/shrimp509/my-img-host/blob/master/relacs-studio/Rails%E6%88%91%E8%A6%81%E9%80%B2%E4%BE%86%E5%9B%89/day6-1.png?raw=true

ActiveJob 負責處理 Background Job

所謂的 background job 就是要在 Rails 處理 request 之外來執行,例如:定時清除訪客資料,這件事就不是根據每個 requests 來做的,而是每一段時間就讓系統自動去執行,而且希望這些背景作業不要影響到原本 Rails request 的運作,也有些任務是會花大量處理時間,像是檔案管理、圖片處理、影片處理等等,這就不適合在 request 期間處理,這就是 background job 的使用時機

這些 background job 的運作原理,通常會在 rails 的 process 之外,在另外開個 process 來管理 background job 的執行順序、執行狀態的控制,會根據不同的 queuing backend 的設計方式不同,下面會跟大家解釋。

ActiveJob 和 Sidekiq 有什麼不同?

我看到 ActiveJob 的第一個反應是,那 ActiveJobSidekiq 的差別是啥?,因為我之前只有用過 Sidekiq,不自覺就會有這樣的疑問

但實際上,處理 background job 的 gem,不只有 Sidekiq,還有 Delayed JobResque,每個 gem 的使用方式不太一樣,使用介面更是不會一樣

我們假設不知道有 ActiveJob 的存在,今天如果專案是使用 Sidekiq,但因為某些原因,Sidekiq 不維護了(這只是假設,不怕不怕XD),逼不得已,必須換成 Delayed Job,那應該會很痛苦,會有很多地方要改

所以,這就是 ActiveJob 存在的意義了!

ActiveJob 的意義

ActiveJob 提供開發者統一處理 background job 的介面,讓開發者只要使用 ActiveJob 的介面來開發,至於後端的 background gem 是使用哪個 gem 就不用太在意,優點就是開發者如果要抽換掉不同的 background gem,是很簡單的一件事,只需要在 ActiveJob 換掉幾個參數就好,但缺點也比較硬傷,就是只能使用 ActiveJob 有整合的 background gem,如果今天出了一個新的、又好用的 background gem,可是 ActiveJob 還沒整合進去的話,那你就沒辦法直接在 ActiveJob 替換掉,官方在 ActiveJob 7.0.3.1 版本有支援的 gem 有 Sidekiq, Resque, Backburner, Delayed Job, Que, queue_classic, , Sneakers, Sucker Punch, Active Job Async Job, Active Job Inline,可以參考官方文件,以 github star 來看熱門度的話,應該是 Sidekiq 最熱門,有 12k 的星星,第二。的是高 Resque,有接近 10k 的星星。

官方提供的各種 background gem 比較表

官方提供的各種 background gem 比較表

ActiveJob 的預設 Queuing Backend

如果不特別設定 background gem 的話,那麼也沒關係,因為 ActiveJob 有自帶一個簡易的 Queueing Backend,不過有個缺點,就是它是把 job 存在 RAM 裡的(in-process queuing system),所以如果機器重開、或是不知道什麼原因導致 crash 掉,這個 job 就會遺失,如果是不那麼重要的 job 或是不那麼重要的 app 的話,那應該沒差,不過如果是要上到 production 的話,官方還是建議要指定一下 background gem,讓專業的來

ActiveJob 的使用方式

建立 Job

$ rails generate job your_job_name
invoke  test_unit
create    test/jobs/your_job_name_job_test.rb
create  app/jobs/your_job_name_job.rb

ActiveJob 會自動幫你的 your_job_name 加上 _job,如果你取的名字本來就有 _job 了,那他就不會幫你加上

產生出來的 job 就會長下面這樣 (官方是用 $ rails generate job guests_cleanup 來產生的)

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  def perform(*guests)
    # Do something later
  end
end

把 Job 塞到 queue 去排隊,等待執行

# 馬上放到 queue 排隊,只要 queue 有空就會執行 
GuestsCleanupJob.perform_later guest

# 事先塞到 queue 裡,指定執行時間
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)  

# 事先塞到 queue 裡,指定一段時間後執行
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)

指定特定的 queuing backend

如果要指定的話,記得要在 Gemfile 先把相關的 gem 裝起來,Sidekiq 和 Resque 都是以 redis 為基礎的 queuing system,所以除了 sidekiq 和 resque 要裝之外,redis 也要裝

# config/application.rb
module YourApp
  class Application < Rails::Application
    # Be sure to have the adapter's gem in your Gemfile
    # and follow the adapter's specific installation
    # and deployment instructions.
    config.active_job.queue_adapter = :sidekiq
  end
end

也可以針對不同的 Job 來設定 queuing backend

class GuestsCleanupJob < ApplicationJob
  self.queue_adapter = :resque
  # ...
end

如果你是選擇使用 Sidekiq 的話

還有一些前置作業,需要搭配 sidekiq 的 github wiki 服用,以下我簡單列出基本且必須的設置

  1. 架設 Redis

    1. sidekiq 預設會連 127.0.0.1:6379 當作是 redis,6379 也是 redis 的預設 port

    2. 你如果會用 docker 的話,那更簡單了,直接開個 redis container 並開出 6379 port 就可以了

    3. $ docker run --name redis -p 6379:6379 -itd redis:latest

    4. 如果要設定別的 host、別的 port 的話,可以在 config/initializers/sidekiq.rb (這個檔案要自己新增),更詳細的設定就參考這裡

      Sidekiq.configure_server do |config|
        config.redis = { url: 'redis://redis.example.com:7372/0' }
      end
      
      Sidekiq.configure_client do |config|
        config.redis = { url: 'redis://redis.example.com:7372/0' }
      end
      
  2. 架設 Sidekiq Process

    1. 還記得最前面講的,background jobs 會獨立運行於 rails 之外,所以除了原本的 rails server 要跑之外,還要再開另一個 process 執行 sidekiq 才行

    2. 執行 sidekiq(記得先把 redis 開在本地 6379 port)

      $ sidekiq
                     `$b
                .ss,  $$:         .,d$
                `$$P,d$P'    .,md$P"'
                 ,$$$$$b/md$$$P^'
               .d$$$$$$/$$$P'
               $$^' `"/$$$'       ____  _     _      _    _
               $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
               `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
                      $$:         ___) | | (_| |  __/   <| | (_| |
                      $$         |____/|_|\__,_|\___|_|\_\_|\__, |
                    .d$$                                       |_|
      
      2022-09-08T02:17:46.309Z pid=12157 tid=77t INFO: Booted Rails 7.0.3.1 application in development environment
      2022-09-08T02:17:46.309Z pid=12157 tid=77t INFO: Running in ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [arm64-darwin20]
      2022-09-08T02:17:46.309Z pid=12157 tid=77t INFO: See LICENSE and the LGPL-3.0 for licensing details.
      2022-09-08T02:17:46.309Z pid=12157 tid=77t INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org
      2022-09-08T02:17:46.309Z pid=12157 tid=77t INFO: Booting Sidekiq 6.5.6 with Sidekiq::RedisConnection::RedisAdapter options {}
      2022-09-08T02:17:46.345Z pid=12157 tid=77t INFO: Starting processing, hit Ctrl-C to stop
      
  3. 你就可以讓你的 background job 開始跑了

    1. 試著用一開始產生的 GuestsCleanupJob 去跑跑看

      class GuestsCleanupJob < ApplicationJob
        queue_as :default
      
        def perform(*guests)
      		File.open("#{Rails.root}/tmp/cleanup.log", "a") do |file|
      			file.write("#{DateTime.current}: 執行 GuestsCleanupJob,參數為 #{guests}\n")
      		end
      	end
      end
      
    2. 我們開個 rails console 來把 Job 塞進去 queue 吧

      $ rails c
      3.0.0 :001 > GuestsCleanupJob.perform_later(1,2,3)
      Enqueued GuestsCleanupJob (Job ID: bb7f38fb-0d66-4cfa-93e8-d7e34afd5eb5) to Sidekiq(default) with arguments: 1, 2, 3
       =>
      #<GuestsCleanupJob:0x000000012ff266d0
       @arguments=[1, 2, 3],
       @exception_executions={},
       @executions=0,
       @job_id="bb7f38fb-0d66-4cfa-93e8-d7e34afd5eb5",
       @priority=nil,
       @provider_job_id="385b37969e546b3471099adf",
       @queue_name="default",
       @successfully_enqueued=true,
       @timezone="UTC">
      

      這時 Job 執行後的結果已經寫入 log 了,表示已經執行完畢

      # tmp/cleanup.log
      2022-09-08T02:37:35+00:00: 執行 GuestsCleanupJob,參數為 [1, 2, 3]
      
  4. [Optional] 把 sidekiq 的監控介面加入 routes

    # config/routes.rb
    require 'sidekiq/web'
    
    Rails.application.routes.draw do
      mount Sidekiq::Web => "/sidekiq"
    	...
    end
    

    ps. 如果你是 api mode 的話,需要加入 session 的 middleware 才行,參考這裡

    然後打開 127.0.0.1:3000/sidekiq 就可以看到已經有一個已處理的任務囉~

    https://raw.githubusercontent.com/shrimp509/my-img-host/master/relacs-studio/Rails%E6%88%91%E8%A6%81%E9%80%B2%E4%BE%86%E5%9B%89/day6-3.png

  5. [Advance] 如果你想要區分不同優先程度的任務的話,可以設置多個 queue,預設已經有 default queue,你也可以加入 urgent queue,有兩個 queue 的話,那就需要兩個 process 來執行

    1. $ sidekiq -q default: 這樣這個 queue 就只會執行 default 的 jobs
    2. $ sidekiq -q urgent: 這個 queue 就只會執行 urgent 的 jobs

總結

這篇 RailsGuide 下面還有一些關於 queue 的操作細節,像是有些 job 在不同情況之下,會適合用不同的 queue 來處理,就需要動態調整 queue,例如有些付費用戶的任務會比較優先處理等等的需求,ActiveJob 還有提供 callback 使用,可能在執行 job 的前後需要做一些前處理、後處理,提供 before_enqueue, around_enqueue, after_enqueue, before_perform, around_perform, after_perform 六種 callbacks,除了以上這些,還有一些細節的操控,就請大家需要的時候自己看文件了~

或許有用 ActiveJob 和 沒有用 ActiveJob 的差別不大,我之前是沒有透過 ActiveJob 來處理 background jobs,而是直接使用 Sidekiq,覺得用起來也蠻直覺的,只是萬一真的有一天必須把 Sidekiq 換掉的話,那真的會是大工程;但對於小專案來說,那還真的是沒有太大差別。

今天這篇就這樣囉,明天來看跟 ActiveJob 有點關聯的東東 - ActiveMailer,我們明天見~


上一篇
第五天:稍微深入 ActiveSupport 一點點,一起來看點 source code
下一篇
第七天:ActionMailer 跟 Controller 很像!?
系列文
Rails,我要進來囉30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言